use crate::address::{HostWithOptPort, HostWithPort};
use crate::forwarded::Forwarded;
use crate::proxy::ProxyTarget;
use crate::transport::{TransportContext, TransportProtocol, TryRefIntoTransportContext};
use crate::{
    Protocol,
    address::{Domain, Host},
};
use rama_core::error::OpaqueError;
use rama_core::extensions::Extensions;
use rama_core::telemetry::tracing;
use rama_http_types::request::Parts;
use rama_http_types::{HttpRequestParts, Request};
use rama_http_types::{Uri, Version};

#[cfg(feature = "tls")]
use crate::tls::SecureTransport;

#[cfg(feature = "tls")]
fn try_get_sni_from_secure_transport(t: &SecureTransport) -> Option<Domain> {
    use crate::tls::client::ClientHelloExtension;

    t.client_hello().and_then(|h| {
        h.extensions().iter().find_map(|e| match e {
            ClientHelloExtension::ServerName(maybe_domain) => maybe_domain.clone(),
            _ => None,
        })
    })
}

#[cfg(not(feature = "tls"))]
#[derive(Debug, Clone)]
#[non_exhaustive]
struct SecureTransport;

#[cfg(not(feature = "tls"))]
fn try_get_sni_from_secure_transport(_: &SecureTransport) -> Option<Domain> {
    None
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// The context of the [`Request`].
pub struct RequestContext {
    /// The HTTP Version.
    pub http_version: Version,
    /// The [`Protocol`] of the [`Request`].
    pub protocol: Protocol,
    /// The authority of the [`Request`].
    ///
    /// In http/1.1 this is typically defined by the `Host` header,
    /// whereas for h2 and h3 this is found in the pseudo `:authority` header.
    ///
    /// This can be also manually set in case there is support for
    /// forward headers (e.g. `Forwarded`, or `X-Forwarded-Host`)
    /// or forward protocols (e.g. `HaProxy`).
    pub authority: HostWithOptPort,
}

impl RequestContext {
    /// Check if the authority is using the default port for the [`Protocol`] set in this [`RequestContext`]
    #[must_use]
    pub fn authority_has_default_port(&self) -> bool {
        self.protocol.default_port() == self.authority.port
    }
}

impl TryFrom<&Parts> for RequestContext {
    type Error = OpaqueError;

    fn try_from(parts: &Parts) -> Result<Self, Self::Error> {
        try_request_ctx_from_http_parts(parts)
    }
}

impl<Body> TryFrom<&Request<Body>> for RequestContext {
    type Error = OpaqueError;

    fn try_from(req: &Request<Body>) -> Result<Self, Self::Error> {
        try_request_ctx_from_http_parts(req)
    }
}

pub fn try_request_ctx_from_http_parts(
    parts: impl HttpRequestParts,
) -> Result<RequestContext, OpaqueError> {
    let uri = parts.uri();

    let protocol = protocol_from_uri_or_extensions(parts.extensions(), uri);
    tracing::trace!(
        url.full = %uri,
        "request context: detected protocol: {protocol} (scheme: {:?}",
        uri.scheme(),
    );

    let default_port = uri
        .port_u16()
        .unwrap_or_else(|| protocol.default_port().unwrap_or(80));
    tracing::trace!(url.full = %uri, "request context: detected default port: {default_port}");

    let authority = uri
        .host()
        .and_then(|h| Host::try_from(h).ok().map(|h| {
            tracing::trace!(url.full = %uri, "request context: detected host {h} from (abs) uri");
            (h, default_port).into()
        }))
        .or_else(|| {
            parts
                .extensions()
                .get::<ProxyTarget>()
                .and_then(|t| {
                    t.0.host.is_domain().then(|| {
                        let authority = t.0.clone().into();
                        tracing::trace!(url.full = %uri, "request context: use proxy target as authority: {authority}");
                        authority
                    })
                })
        })
        .or_else(|| {
            parts
                .extensions()
                .get()
                .and_then(try_get_sni_from_secure_transport)
                .map(|host| {
                    tracing::trace!(url.full = %uri, "request context: detected host {host} from SNI");
                    (host, default_port).into()
                })
        })
        .or_else(|| {
            parts.extensions().get::<Forwarded>().and_then(|f| {
                f.client_host().map(|fauth| {
                    let HostWithOptPort { host, port } = fauth.0.clone();
                    let port = port.unwrap_or(default_port);
                    tracing::trace!(url.full = %uri, "request context: detected host {host} from forwarded info");
                    (host, port).into()
                })
            })
        })
        .or_else(|| {
            parts.headers()
                .get(rama_http_types::header::HOST)
                .and_then(|host_header_value| {
                    HostWithOptPort::try_from(host_header_value.as_bytes()).ok()
                })
        })
        .ok_or_else(|| {
            OpaqueError::from_display("RequestContext: no authourity found in http::Request")
        })?;

    tracing::trace!(url.full = %uri, "request context: detected authority: {authority}");

    let http_version = parts
        .extensions()
        .get::<Forwarded>()
        .and_then(|f| {
            f.client_version().map(|v| match v {
                crate::forwarded::ForwardedVersion::HTTP_09 => Version::HTTP_09,
                crate::forwarded::ForwardedVersion::HTTP_10 => Version::HTTP_10,
                crate::forwarded::ForwardedVersion::HTTP_11 => Version::HTTP_11,
                crate::forwarded::ForwardedVersion::HTTP_2 => Version::HTTP_2,
                crate::forwarded::ForwardedVersion::HTTP_3 => Version::HTTP_3,
            })
        })
        .unwrap_or_else(|| parts.version());
    tracing::trace!(url.full = %uri, "request context: maybe detected http version: {http_version:?}");

    Ok(RequestContext {
        http_version,
        protocol,
        authority,
    })
}

#[allow(clippy::unnecessary_lazy_evaluations)]
fn protocol_from_uri_or_extensions(ext: &Extensions, uri: &Uri) -> Protocol {
    uri.scheme().map(Protocol::from).or_else(|| {
        // Can be inserted by a server stack to notify the protocol that's being served.
        // This is especially useful for marking a HTTPS server as HTTPS,
        // despite it not showing up anywhere due to a non-default port
        // and it being http/1
        ext.get::<Protocol>().cloned()
    }).or_else(|| ext.get::<Forwarded>()
        .and_then(|f| f.client_proto().map(|p| {
            tracing::trace!(url.furi = %uri, "request context: detected protocol from forwarded client proto");
            p.into()
        })))
        .unwrap_or_else(||
    if ext.contains::<SecureTransport>() {
        Protocol::HTTPS
    } else {
        Protocol::HTTP
    })
}

impl RequestContext {
    #[must_use]
    pub fn host_with_port(&self) -> HostWithPort {
        let port = self
            .authority
            .port
            .or_else(|| self.protocol.default_port())
            .unwrap_or(Protocol::HTTP_DEFAULT_PORT);
        let host = self.authority.host.clone();
        HostWithPort { host, port }
    }
}

impl From<RequestContext> for TransportContext {
    fn from(value: RequestContext) -> Self {
        Self {
            protocol: if value.http_version == Version::HTTP_3 {
                TransportProtocol::Udp
            } else {
                TransportProtocol::Tcp
            },
            app_protocol: Some(value.protocol),
            http_version: Some(value.http_version),
            authority: value.authority,
        }
    }
}

impl From<&RequestContext> for TransportContext {
    fn from(value: &RequestContext) -> Self {
        Self {
            protocol: if value.http_version == Version::HTTP_3 {
                TransportProtocol::Udp
            } else {
                TransportProtocol::Tcp
            },
            app_protocol: Some(value.protocol.clone()),
            http_version: Some(value.http_version),
            authority: value.authority.clone(),
        }
    }
}

impl<Body> TryRefIntoTransportContext for rama_http_types::Request<Body> {
    type Error = OpaqueError;

    fn try_ref_into_transport_ctx(&self) -> Result<TransportContext, Self::Error> {
        self.try_into()
    }
}

impl TryRefIntoTransportContext for rama_http_types::request::Parts {
    type Error = OpaqueError;

    fn try_ref_into_transport_ctx(&self) -> Result<TransportContext, Self::Error> {
        self.try_into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::forwarded::{Forwarded, ForwardedElement, NodeId};
    use rama_core::extensions::ExtensionsMut;
    use rama_http_types::{Request, header::FORWARDED};

    #[test]
    fn test_request_context_from_request() {
        let req = Request::builder()
            .uri("http://example.com:8080")
            .version(Version::HTTP_11)
            .body(())
            .unwrap();

        let req_ctx = RequestContext::try_from(&req).unwrap();

        assert_eq!(req_ctx.http_version, Version::HTTP_11);
        assert_eq!(req_ctx.protocol, Protocol::HTTP);
        assert_eq!(req_ctx.authority.to_string(), "example.com:8080");
    }

    #[test]
    fn test_request_context_from_parts() {
        let req = Request::builder()
            .uri("http://example.com:8080")
            .version(Version::HTTP_11)
            .body(())
            .unwrap();

        let (parts, _) = req.into_parts();

        let req_ctx = RequestContext::try_from(&parts).unwrap();

        assert_eq!(req_ctx.http_version, Version::HTTP_11);
        assert_eq!(req_ctx.protocol, Protocol::HTTP);
        assert_eq!(
            req_ctx.authority,
            HostWithOptPort::try_from("example.com:8080").unwrap()
        );
    }

    #[test]
    fn test_request_context_authority() {
        let ctx = RequestContext {
            http_version: Version::HTTP_11,
            protocol: Protocol::HTTP,
            authority: "example.com:8080".try_into().unwrap(),
        };

        assert_eq!(ctx.authority.to_string(), "example.com:8080");
    }

    #[test]
    fn forwarded_parsing() {
        for (forwarded_str_vec, expected) in [
            // base
            (
                vec!["host=192.0.2.60;proto=http;by=203.0.113.43"],
                RequestContext {
                    http_version: Version::HTTP_11,
                    protocol: Protocol::HTTP,
                    authority: "192.0.2.60:80".parse().unwrap(),
                },
            ),
            // ipv6
            (
                vec!["host=\"[2001:db8:cafe::17]:4711\""],
                RequestContext {
                    http_version: Version::HTTP_11,
                    protocol: Protocol::HTTP,
                    authority: "[2001:db8:cafe::17]:4711".parse().unwrap(),
                },
            ),
            // multiple values in one header
            (
                vec!["host=192.0.2.60, host=127.0.0.1"],
                RequestContext {
                    http_version: Version::HTTP_11,
                    protocol: Protocol::HTTP,
                    authority: "192.0.2.60:80".parse().unwrap(),
                },
            ),
            // multiple header values
            (
                vec!["host=192.0.2.60", "host=127.0.0.1"],
                RequestContext {
                    http_version: Version::HTTP_11,
                    protocol: Protocol::HTTP,
                    authority: "192.0.2.60:80".parse().unwrap(),
                },
            ),
        ] {
            let mut req_builder = Request::builder();
            for header in forwarded_str_vec.clone() {
                req_builder = req_builder.header(FORWARDED, header);
            }

            let mut req = req_builder.body(()).unwrap();

            let forwarded: Forwarded = req.headers().get(FORWARDED).unwrap().try_into().unwrap();
            req.extensions_mut().insert(forwarded);

            let req_ctx = RequestContext::try_from(&req).unwrap();

            assert_eq!(req_ctx, expected, "Failed for {forwarded_str_vec:?}");
        }
    }

    #[test]
    fn test_request_ctx_https_request_behind_haproxy_plain() {
        let mut req = Request::builder()
            .uri("/en/reservation/roomdetails")
            .version(Version::HTTP_11)
            .header("host", "echo.ramaproxy.org")
            .header("user-agent", "curl/8.6.0")
            .header("accept", "*/*")
            .body(())
            .unwrap();

        req.extensions_mut()
            .insert(Forwarded::new(ForwardedElement::new_forwarded_for(
                NodeId::try_from("127.0.0.1:61234").unwrap(),
            )));

        let req_ctx = RequestContext::try_from(&req).unwrap();

        assert_eq!(req_ctx.http_version, Version::HTTP_11);
        assert_eq!(req_ctx.protocol, "http");
        assert_eq!(req_ctx.authority.to_string(), "echo.ramaproxy.org");
        assert_eq!(
            req_ctx.host_with_port().to_string(),
            "echo.ramaproxy.org:80"
        );
    }
}
